package astiav

//#include <libavformat/avformat.h>
//#include "io_context.h"
import "C"
import (
	"errors"
	"fmt"
	"io"
	"sync"
	"unsafe"
)

// https://ffmpeg.org/doxygen/8.0/structAVIOContext.html
type IOContext struct {
	c         *C.AVIOContext
	handlerID unsafe.Pointer
}

func newIOContextFromC(c *C.AVIOContext) *IOContext {
	if c == nil {
		return nil
	}
	ic := &IOContext{c: c}
	classers.set(ic)
	return ic
}

var _ Classer = (*IOContext)(nil)

type IOContextReadFunc func(b []byte) (n int, err error)

type IOContextSeekFunc func(offset int64, whence int) (n int64, err error)

type IOContextWriteFunc func(b []byte) (n int, err error)

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#a50c588d3c44707784f3afde39e1c181c
func AllocIOContext(bufferSize int, writable bool, readFunc IOContextReadFunc, seekFunc IOContextSeekFunc, writeFunc IOContextWriteFunc) (ic *IOContext, err error) {
	// Invalid buffer size
	if bufferSize <= 0 {
		err = errors.New("astiav: buffer size <= 0")
		return
	}

	// Allocate buffer
	buffer := C.av_malloc(C.size_t(bufferSize))
	if buffer == nil {
		err = errors.New("astiav: allocating buffer failed")
		return
	}

	// Make sure buffer is freed in case of error
	defer func() {
		if err != nil {
			C.av_free(buffer)
		}
	}()

	// Since go doesn't allow c to store pointers to go data, we need to create this C pointer
	handlerID := C.av_malloc(C.size_t(1))
	if handlerID == nil {
		err = errors.New("astiav: allocating handler id failed")
		return
	}

	// Make sure handler id is freed in case of error
	defer func() {
		if err != nil {
			C.av_free(handlerID)
		}
	}()

	// Get callbacks
	var cReadFunc, cSeekFunc, cWriteFunc *[0]byte
	if readFunc != nil {
		cReadFunc = (*[0]byte)(C.astiavIOContextReadFunc)
	}
	if seekFunc != nil {
		cSeekFunc = (*[0]byte)(C.astiavIOContextSeekFunc)
	}
	if writeFunc != nil {
		cWriteFunc = (*[0]byte)(C.astiavIOContextWriteFunc)
	}

	// Get write flag
	wf := C.int(0)
	if writable {
		wf = C.int(1)
	}

	// Allocate io context
	cic := C.avio_alloc_context((*C.uchar)(buffer), C.int(bufferSize), wf, handlerID, cReadFunc, cWriteFunc, cSeekFunc)
	if cic == nil {
		err = errors.New("astiav: allocating io context failed: %w")
		return
	}

	// Create io context
	ic = newIOContextFromC(cic)

	// Store handler
	ic.handlerID = handlerID
	ioContextHandlers.set(handlerID, &ioContextHandler{
		r: readFunc,
		s: seekFunc,
		w: writeFunc,
	})
	return
}

// https://ffmpeg.org/doxygen/8.0/avio_8c.html#ae8589aae955d16ca228b6b9d66ced33d
func OpenIOContext(filename string, flags IOContextFlags, ii *IOInterrupter, d *Dictionary) (*IOContext, error) {
	cfi := C.CString(filename)
	defer C.free(unsafe.Pointer(cfi))
	var dc **C.AVDictionary
	if d != nil {
		dc = &d.c
	}
	var cii *C.AVIOInterruptCB = nil
	if ii != nil {
		cii = ii.c
	}
	var c *C.AVIOContext
	if err := newError(C.avio_open2(&c, cfi, C.int(flags), cii, dc)); err != nil {
		return nil, err
	}
	return newIOContextFromC(c), nil
}

func (ic *IOContext) Class() *Class {
	if ic.c == nil {
		return nil
	}
	return newClassFromC(unsafe.Pointer(ic.c))
}

// https://ffmpeg.org/doxygen/8.0/avio_8c.html#ae118a1f37f1e48617609ead9910aac15
func (ic *IOContext) Close() error {
	if ic.c != nil {
		// Make sure to clone the classer before freeing the object since
		// the C free method may reset the pointer
		c := newClonedClasser(ic)
		// Error is returned when closing the url but pointer has been freed at this point
		// therefore we must make sure classers are cleaned up properly even on error
		err := newError(C.avio_closep(&ic.c))
		// Make sure to remove from classers after freeing the object since
		// the C free method may use methods needing the classer
		if c != nil && ic.c == nil {
			classers.del(c)
		}
		return err
	}
	return nil
}

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#ad1baf8cd6711f05a45d0339cafe2d21d
func (ic *IOContext) Free() {
	if ic.c != nil {
		if ic.c.buffer != nil {
			C.av_freep(unsafe.Pointer(&ic.c.buffer))
		}
		if ic.handlerID != nil {
			C.av_free(ic.handlerID)
			ic.handlerID = nil
		}
		// Make sure to clone the classer before freeing the object since
		// the C free method may reset the pointer
		c := newClonedClasser(ic)
		C.avio_context_free(&ic.c)
		// Make sure to remove from classers after freeing the object since
		// the C free method may use methods needing the classer
		if c != nil {
			classers.del(c)
		}
	}
	return
}

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#a53843d2cbe6282d994fcf59c03d59294
func (ic *IOContext) Read(b []byte) (n int, err error) {
	// Nothing to read
	if b == nil || len(b) <= 0 {
		return
	}

	// Allocate buffer
	buf := C.av_malloc(C.size_t(len(b)))
	if buf == nil {
		err = errors.New("astiav: allocating buffer failed")
		return
	}

	// Make sure buffer is freed
	defer C.av_free(buf)

	// Read
	ret := C.avio_read_partial(ic.c, (*C.uchar)(unsafe.Pointer(buf)), C.int(len(b)))
	if err = newError(ret); err != nil {
		err = fmt.Errorf("astiav: reading failed: %w", err)
		return
	}

	// Copy
	C.memcpy(unsafe.Pointer(&b[0]), unsafe.Pointer(buf), C.size_t(ret))
	n = int(ret)
	return
}

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#a03e23bf0144030961c34e803c71f614f
func (ic *IOContext) Seek(offset int64, whence int) (int64, error) {
	ret := C.avio_seek(ic.c, C.int64_t(offset), C.int(whence))
	if err := newError(C.int(ret)); err != nil {
		return 0, err
	}
	return int64(ret), nil
}

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#acc3626afc6aa3964b75d02811457164e
func (ic *IOContext) Write(b []byte) {
	// Nothing to write
	if b == nil || len(b) <= 0 {
		return
	}

	// Write
	C.avio_write(ic.c, (*C.uchar)(unsafe.Pointer(&b[0])), C.int(len(b)))
}

// https://ffmpeg.org/doxygen/8.0/avio_8h.html#ad88b866a118c17c95663f7782b2e8946
func (ic *IOContext) Flush() {
	C.avio_flush(ic.c)
}

type ioContextHandler struct {
	r IOContextReadFunc
	s IOContextSeekFunc
	w IOContextWriteFunc
}

var ioContextHandlers = newIOContextHandlerPool()

type ioContextHandlerPool struct {
	m sync.Mutex
	p map[unsafe.Pointer]*ioContextHandler
}

func newIOContextHandlerPool() *ioContextHandlerPool {
	return &ioContextHandlerPool{p: make(map[unsafe.Pointer]*ioContextHandler)}
}

func (p *ioContextHandlerPool) set(id unsafe.Pointer, h *ioContextHandler) {
	p.m.Lock()
	defer p.m.Unlock()
	p.p[id] = h
}

func (p *ioContextHandlerPool) get(id unsafe.Pointer) (h *ioContextHandler, ok bool) {
	p.m.Lock()
	defer p.m.Unlock()
	h, ok = p.p[id]
	return
}

//export goAstiavIOContextReadFunc
func goAstiavIOContextReadFunc(opaque unsafe.Pointer, buf *C.uint8_t, bufSize C.int) C.int {
	// Get handler
	h, ok := ioContextHandlers.get(opaque)
	if !ok {
		return C.AVERROR_UNKNOWN
	}

	// Create go buffer
	b := make([]byte, int(bufSize), int(bufSize))

	// Read
	n, err := h.r(b)
	if err != nil {
		var e Error
		if errors.As(err, &e) {
			return C.int(e)
		} else if errors.Is(err, io.EOF) {
			return C.AVERROR_EOF
		}
		return C.AVERROR_UNKNOWN
	}

	// Copy
	C.memcpy(unsafe.Pointer(buf), unsafe.Pointer(&b[0]), C.size_t(n))
	return C.int(n)
}

//export goAstiavIOContextSeekFunc
func goAstiavIOContextSeekFunc(opaque unsafe.Pointer, offset C.int64_t, whence C.int) C.int64_t {
	// Get handler
	h, ok := ioContextHandlers.get(opaque)
	if !ok {
		return C.AVERROR_UNKNOWN
	}

	// Seek
	n, err := h.s(int64(offset), int(whence))
	if err != nil {
		var e Error
		if errors.As(err, &e) {
			return C.int64_t(e)
		}
		return C.int64_t(C.AVERROR_UNKNOWN)
	}
	return C.int64_t(n)
}

//export goAstiavIOContextWriteFunc
func goAstiavIOContextWriteFunc(opaque unsafe.Pointer, buf *C.uint8_t, bufSize C.int) C.int {
	// Get handler
	h, ok := ioContextHandlers.get(opaque)
	if !ok {
		return C.AVERROR_UNKNOWN
	}

	// Write
	n, err := h.w(C.GoBytes(unsafe.Pointer(buf), bufSize))
	if err != nil {
		var e Error
		if errors.As(err, &e) {
			return C.int(e)
		}
		return C.AVERROR_UNKNOWN
	}
	return C.int(n)
}

// AvioFindProtocolName finds the protocol name from a URL
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func AvioFindProtocolName(url string) string {
	cURL := C.CString(url)
	defer C.free(unsafe.Pointer(cURL))
	
	cProtocol := C.avio_find_protocol_name(cURL)
	if cProtocol == nil {
		return ""
	}
	return C.GoString(cProtocol)
}

// AvioEnum enumerates available protocols
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func AvioEnum(output bool) []string {
	var protocols []string
	var opaque unsafe.Pointer
	
	for {
		var cProtocol *C.char
		if output {
			cProtocol = C.avio_enum_protocols(&opaque, 1)
		} else {
			cProtocol = C.avio_enum_protocols(&opaque, 0)
		}
		
		if cProtocol == nil {
			break
		}
		
		protocols = append(protocols, C.GoString(cProtocol))
	}
	
	return protocols
}

// IODirEntryType represents the type of a directory entry
type IODirEntryType int

const (
	IODirEntryTypeUnknown         IODirEntryType = C.AVIO_ENTRY_UNKNOWN
	IODirEntryTypeFile            IODirEntryType = C.AVIO_ENTRY_FILE
	IODirEntryTypeDirectory       IODirEntryType = C.AVIO_ENTRY_DIRECTORY
	IODirEntryTypeBlockDevice     IODirEntryType = C.AVIO_ENTRY_BLOCK_DEVICE
	IODirEntryTypeCharacterDevice IODirEntryType = C.AVIO_ENTRY_CHARACTER_DEVICE
	IODirEntryTypeNamedPipe       IODirEntryType = C.AVIO_ENTRY_NAMED_PIPE
	IODirEntryTypeSymbolicLink    IODirEntryType = C.AVIO_ENTRY_SYMBOLIC_LINK
	IODirEntryTypeSocket          IODirEntryType = C.AVIO_ENTRY_SOCKET
	IODirEntryTypeServer          IODirEntryType = C.AVIO_ENTRY_SERVER
	IODirEntryTypeShare           IODirEntryType = C.AVIO_ENTRY_SHARE
	IODirEntryTypeWorkgroup       IODirEntryType = C.AVIO_ENTRY_WORKGROUP
)

// IODirContext represents a directory context for listing files
type IODirContext struct {
	c *C.AVIODirContext
}

// IODirEntry represents a directory entry
type IODirEntry struct {
	c *C.AVIODirEntry
}

// OpenDir opens a directory for listing
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func OpenDir(url string, options *Dictionary) (*IODirContext, error) {
	var cOptions *C.AVDictionary
	if options != nil {
		cOptions = options.c
	}
	
	cURL := C.CString(url)
	defer C.free(unsafe.Pointer(cURL))
	
	var cCtx *C.AVIODirContext
	ret := C.avio_open_dir(&cCtx, cURL, &cOptions)
	if ret < 0 {
		return nil, newError(ret)
	}
	
	return &IODirContext{c: cCtx}, nil
}

// ReadDir reads the next directory entry
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ctx *IODirContext) ReadDir() (*IODirEntry, error) {
	var cEntry *C.AVIODirEntry
	ret := C.avio_read_dir(ctx.c, &cEntry)
	if ret < 0 {
		return nil, newError(ret)
	}
	if cEntry == nil {
		return nil, nil // End of directory
	}
	
	return &IODirEntry{c: cEntry}, nil
}

// CloseDir closes the directory context
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ctx *IODirContext) CloseDir() error {
	ret := C.avio_close_dir(&ctx.c)
	if ret < 0 {
		return newError(ret)
	}
	return nil
}

// Free frees the directory entry
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (entry *IODirEntry) Free() {
	C.avio_free_directory_entry(&entry.c)
}

// Name returns the name of the directory entry
func (entry *IODirEntry) Name() string {
	if entry.c.name == nil {
		return ""
	}
	return C.GoString(entry.c.name)
}

// Type returns the type of the directory entry
func (entry *IODirEntry) Type() IODirEntryType {
	return IODirEntryType(entry.c._type)
}

// Size returns the size of the directory entry
func (entry *IODirEntry) Size() int64 {
	return int64(entry.c.size)
}

// Filemode returns the file mode of the directory entry
func (entry *IODirEntry) Filemode() int64 {
	return int64(entry.c.filemode)
}

// UserID returns the user ID of the directory entry
func (entry *IODirEntry) UserID() int64 {
	return int64(entry.c.user_id)
}

// GroupID returns the group ID of the directory entry
func (entry *IODirEntry) GroupID() int64 {
	return int64(entry.c.group_id)
}

// ModificationTimestamp returns the modification timestamp of the directory entry
func (entry *IODirEntry) ModificationTimestamp() int64 {
	return int64(entry.c.modification_timestamp)
}

// AccessTimestamp returns the access timestamp of the directory entry
func (entry *IODirEntry) AccessTimestamp() int64 {
	return int64(entry.c.access_timestamp)
}

// StatusChangeTimestamp returns the status change timestamp of the directory entry
func (entry *IODirEntry) StatusChangeTimestamp() int64 {
	return int64(entry.c.status_change_timestamp)
}

// Handshake performs HTTP handshake
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ic *IOContext) Handshake() int {
	return int(C.avio_handshake(ic.c))
}

// Accept accepts a new client connection
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ic *IOContext) Accept() (*IOContext, error) {
	var cClient *C.AVIOContext
	ret := C.avio_accept(ic.c, &cClient)
	if ret < 0 {
		return nil, newError(ret)
	}
	return newIOContextFromC(cClient), nil
}

// GetOption gets an option value
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ic *IOContext) GetOption(name string) (string, error) {
	options := newOptionsFromC(unsafe.Pointer(ic.c))
	return options.Get(name, NewOptionSearchFlags(OptionSearchFlagChildren))
}

// SetOption sets an option value
// https://ffmpeg.org/doxygen/8.0/group__lavf__misc.html#ga7b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b
func (ic *IOContext) SetOption(name, value string) error {
	options := newOptionsFromC(unsafe.Pointer(ic.c))
	return options.Set(name, value, NewOptionSearchFlags(OptionSearchFlagChildren))
}
